Borland Online And The Cobb Group Present:


May, 1995 - Vol. 2 No. 5

Using containers instead of arrays

In the article Advanced C++ programming - Reconstructing objects we show how you can reconstruct an object that's part of an array. Here, we'll review some reasons you should consider using a container class object to maintain a group of objects, instead of an array. First, we'll look at some of the pitfalls of standard C-style arrays, and then we'll discuss the benefits of using a container class object.

Arrays of hope

In the accompanying article's example, we built an array of Wombat objects on the program stack. In a real program, you probably would create an array like this in the program's heap (global memory pool) using

Wombat* wArray = new Wombat[3];

Now, wArray is a pointer to the first Wombat object in the array in addition to being a pointer to the block of memory where the program built the array.

Interestingly, wArray was also a pointer to the first Wombat object in the original program. However, since the original program created the array of Wombat objects on the stack, the array identifier wArray doesn't represent a block of memory.

This becomes obvious when you think about deallocating the memory for the array. If you create the array on the stack, the program will release that memory when the current function exits, and the program will adjust the stack pointer to its previous location (prior to entering the function).

Conversely, if you allocate the array in the pro-gram's heap using new Wombat[3], you'll need to release the memory yourself by calling

delete [] wArray;

at some point in the program. (By the way, you should always use this syntax when deleting an array. This ensures that the program will call the destructor for each of the array elements before releasing the memory.)

Unfortunately, the identifier wArray now has three possible meanings: It's a pointer to a block of memory allocated using new( ), it's a Wombat pointer for the first element in the array, and it's an identifier for the entire array that you can use to address individual elements. When a given identifier has multiple valid meanings in a C++ program, the compiler won't be able to help you identify incorrect uses of that identifier.

For example, it's syntactically valid to write

void Sleep(Wombat* w);

and then call this function using

Sleep(wArray);

When you look at the function's declaration, it isn't obvious whether the function is expecting a pointer to an array of Wombat objects, a pointer to an individual Wombat object, or a pointer to a block of memory on the heap containing more than one Wombat object in an array.

In addition, if you forget to use the brackets ([]) when you delete an array and instead write

delete wArray;

the compiler won't know whether it's supposed to de-lete an array or a single object. (Actually, in Borland C++, the default versions of new and delete will allo-cate and release the correct amount of memory if you forget the brackets. However, the program will call the destructor for the first object in the array only. Custom versions of new and delete may not correctly release all the memory for an array.)

Finally, if you create a new array using the statement

Wombat* wArray = new Wombat[3];

another part of your program could modify this pointer with a statement similar to

while(wArray++)

Unfortunately, if this pointer no longer addresses the first element of this array, indexing into the array using the syntax wArray[1] becomes susceptible to bugs. Likewise, trying to release memory using

delete [] wArray;

is likely to crash the program, since the pointer doesn't address the beginning of the block of memory anymore.

Instead, you can create the new array using

Wombat* const wArray = new Wombat[3];

While this won't prevent you from misusing the identifier wArray in all cases, it will guarantee that it always points to the beginning of the array.

Opening BIDS

Instead of battling the potential abuses of array identifiers and pointers, you can use the Borland International Data Structures (BIDS) container classes that ship with Borland C++, or you can use a third-party collection class library like the Standard Template Library (STL) from Hewlett-Packard. (Even though the standards committee has accepted the STL as part of the C++ specification, we'll consider the STL to be a third-party library until Borland begins shipping it with C++.)

For example, to create a stack-based container of Wombat objects by using the BIDS ArrayAsVector class, you'd write

typedef StackAsVector<Wombat> WombatStack;

WombatStack wArray(3);

Since the identifier wArray is no longer a simple pointer to a Wombat object, the compiler won't let you pass it to the Sleep(Wombat*) function, and the compiler won't let you modify wArray using the wArray++ syntax that's valid for pointers.

One final benefit of using the container classes is that they allow you to change the storage implemen-tation more easily. For example, since C++ doesn't provide direct language support for lists or queues like it does for arrays, changing the storage mechanism for an array can be a difficult task.

If you use the BIDS container classes instead, you can change the storage implementation from a vector (C-style array) to a list by writing

typedef StackAsList<Wombat> WombatStack;

This change won't affect any of the code that used the StackAsVector class member functions, because the StackAsList class provides them as well.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.